iT邦幫忙

2024 iThome 鐵人賽

0
生成式 AI

使用 Spring AI 打造企業 RAG 知識庫系列 第 35

使用 Spring AI 打造企業 RAG 知識庫【35】- React 前端如何處理流式輸出

  • 分享至 

  • xImage
  •  

雖然主題是 Spring AI, 不過沒前端總覺得少了什麼,所以特別整理一篇前端開發類似 ChatGPT 網頁需注意的重點,先來看看成果吧
Yes

這次後端的部分使用 Day4 的程式碼,使用 Spring AI 做流式輸出有個特別的地方 Flux<String> chatModel.stream(prompt) prompt 若使用 String 則回傳得資料也會是 String 的片段,不過也因此在處理上有些莫名其妙的問題,若想使用 Event 的方式處理訊息最好使用 Prompt 物件來傳送內容,這樣回傳資料就會是 Flux<ChatResponse>,前端處理時使用 Json 比較不用處理特殊符號

  1. LLM 輸出的內容基本都是 Markdown 格式,以使用 React 為例,輸出對話使用 MUI 的 List元件,若要能呈現 Markdown 效果還是需要另外安裝 Markdown 元件,另外要支援 code 語法還需要安裝插件

    import ReactMarkdown, { Components } from 'react-markdown';
    import remarkGfm from 'remark-gfm';
    import remarkEmoji from 'remark-emoji';
    import remarkToc from 'remark-toc';
    import remarkMath from 'remark-math';
    import remarkSlug from 'remark-slug';
    import rehypeKatex from 'rehype-katex';
    import rehypeAutolinkHeadings from 'rehype-autolink-headings';
    import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
    import { atomDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
    
    const renderers: Components = {
      code( {node, inline, className, children, ...props}: any ) {
        const match = /language-(\w+)/.exec(className || '');
        return !inline && match ? (
          <SyntaxHighlighter 
            style={ atomDark as any} 
            language={match[1]} 
            PreTag="div" 
            {...props}
          >
            {String(children).replace(/\n$/, '')}
          </SyntaxHighlighter>
        ) : (
          <code className={className} {...props}>
            {children}
          </code>
        );
      }
    };
    
    //下方為 react 元件使用 ReactMarkdown 的方式
    <ReactMarkdown 
        remarkPlugins={[remarkGfm, remarkEmoji, remarkToc, remarkMath, remarkSlug as any]}
        rehypePlugins={[rehypeKatex, rehypeAutolinkHeadings]}  
        components={renderers}>
      {message.text}
    </ReactMarkdown>
    
  2. 流式輸出雖然使用 SSE 技術,不過標準 SSE 只支援 Get 方法,而 Get 方法瀏覽器又不支援傳送 Json,原本想裝微軟的 fetchEventSource,可支援 Post 也能處理 SSE,可是不知為什麼回傳的內容空白都會消失,最後還是用 fetch 加上 useState 來處理打字機效果

  3. 使用 fetch 有以下幾個步驟
    a. 因為 Stream 會不斷推送資料,Reader 讀取後結果會包含 done : boolean 以及 value : Uint8Array,當 done 是 false 時表示資料還沒傳送完,可以寫一個無窮迴圈,當 done 為 true 才跳出迴圈
    b. value 型態是 Uint8Array,須透過 decoder 轉回 utf8
    c. 因為不是使用 Event 方式接收資料,所以可能好幾個片段一起傳送過來,這時可用下面方式取得正確內容

        const jsonChunks = chunk.split('data:').filter(Boolean);
        for (let jsonChunk of jsonChunks){
        	newTemp += jsonChunk.replace(/\n\n/g, '');
       }
    

    SSE 傳送的資料前都會有 data: 所以需要先用這個字串拆成小片段,另外內容可能包含一些空字串,所以後面可再使用 .filter(Boolean) 過濾不要的資料
    最後因為每個 data 最後都是 \n\n 結尾,我們可使用 .replace(/\n\n/g, '') 去除

  4. List 最下面可以放置一個 ListItem,搭配useState處理最新回覆的打字機效果,其他舊訊息使用陣列存放,最新回覆處理完後也要加入訊息陣列,陣列的內容需要額外加上是用戶傳送還是 LLM 回傳,透過css就能做到對話的泡泡框

    setMessages([...messages, {text:newMessage, sender: 'bot'}]);
    
  5. 需要中斷回覆要使用 AbortController 元件,並加入傳送的封包中,只需要做一個暫停按鈕觸發 Abort 就能停止傳送資料

    const controller = new AbortController();
    const promptBody:PromptBody = { 'prompt': prompt };
    const response = await fetch('http://localhost:8080/stream', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(promptBody),
      signal: controller.signal, 
    });
    
    //後面需要暫停時呼叫以下指令
    controller.abort();
    

    另外最近 OpenAI 釋出了 realtime 模型,這裡又使用另一個後端主動推送的技術 Websocket,websocket 與一般 http 傳輸最大的差別就是建立連線後雙向可全雙工傳輸資料,而 OpenAI 提供的架構中,用戶並非直接對到 API,而是需要一個中間層作為雙方的橋樑
    https://ithelp.ithome.com.tw/upload/images/20241005/201612905UJoqq5UMy.png
    Spring 不管在 Client 或是 Server 都有套件可以處理 Websocket 內容,有機會凱文大叔再來教大家寫中間轉發 Websocket 的部分


上一篇
使用 Spring AI 打造企業 RAG 知識庫【34】 - 在 Spring AI 實作 Re-ranking
系列文
使用 Spring AI 打造企業 RAG 知識庫35
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言